package com.apes.scm.account.ledger.service;

import com.alibaba.fastjson.JSONObject;
import com.apes.framework.api.DomainService;
import com.apes.framework.jpa.repository.CustomRepository;
import com.apes.framework.jpa.spec.JpaDsl;
import com.apes.framework.plugin.esb.api.SimpleRequest;
import com.apes.framework.plugin.soe.metadata.BusinessType;
import com.apes.framework.plugin.soe.store.database.service.BusinessTypeService;
import com.apes.framework.util.Arith;
import com.apes.framework.util.JpaUtil;
import com.apes.framework.util.MapUtil;
import com.apes.framework.util.ReflectUtil;
import com.apes.scm.account.ledger.model.Ledger;
import com.apes.scm.account.ledger.model.LedgerAccount;
import com.apes.scm.account.ledger.model.LedgerEntry;
import com.apes.scm.account.ledger.model.LedgerGroup;
import com.apes.scm.account.ledger.repository.LedgerAccountRepository;
import com.apes.scm.account.ledger.repository.LedgerDao;
import com.apes.scm.account.ledger.repository.LedgerEntryRepository;
import com.apes.scm.account.ledger.repository.LedgerRepository;
import com.apes.scm.account.method.Batch;
import com.apes.scm.account.method.BatchMethod;
import com.apes.scm.masterdata.frame.model.Dept;
import com.apes.scm.masterdata.party.model.PartyRole;
import lombok.Data;
import org.hibernate.query.internal.NativeQueryImpl;
import org.hibernate.transform.Transformers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import javax.persistence.EntityManager;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.groupingBy;

/**
 * 功能：往来总帐服务
 *
 * @author xul
 * @create 2018-11-03 15:41
 */
@Service
public class LedgerService extends DomainService {

    @Autowired
    private LedgerRepository ledgerRepository;

    @Autowired
    private LedgerEntryRepository ledgerEntryRepository;

    @Autowired
    private LedgerAccountRepository ledgerAccountRepository;

    @Autowired
    private LedgerDao ledgerDao;

    /**
     * 按输入条件汇总总帐
     *
     * @param parameters 输入条件
     * @return
     */
    public Map<String, Double> summary(Map<String, Object> parameters) {
        List<Ledger> ledgers = findLedgers(parameters);
        return ledgers.stream().collect(Collectors.groupingBy(Ledger::acquireLedgerAccountId, Collectors.summingDouble(Ledger::getAmount)));
    }

    public List<Ledger> findLedgers(Map<String, Object> parameters) {
        return ledgerRepository.findAll(JpaDsl.toCriteria(parameters));
    }

    /**
     * 记帐
     *
     * @param company            公司
     * @param dept               部门
     * @param partner            合作伙伴
     * @param inventoryMode      库存方式
     * @param accountingMethod   采购核算方式
     * @param source             来源单号
     * @param sourceBusinessType 来源单据类型
     * @param accountingDate     记帐日期
     * @param ledgerAccountId    帐目类型ID
     * @param incurred           发生金额
     */
    public LedgerEntry charge(PartyRole company, Dept dept, PartyRole partner, String inventoryMode,
                              String accountingMethod, String source, String sourceBusinessType,
                              Date accountingDate, String ledgerAccountId, double incurred) {
        LedgerAccount ledgerAccount = getLedgerAccount(ledgerAccountId);
        return charge(company, dept, partner, inventoryMode, accountingMethod, source, sourceBusinessType, accountingDate, ledgerAccount, incurred);
    }

    /**
     * 记帐
     *
     * @param company            公司
     * @param dept               部门
     * @param partner            合作伙伴
     * @param inventoryMode      库存方式
     * @param accountingMethod   采购核算方式
     * @param source             来源单号
     * @param sourceBusinessType 来源单据类型
     * @param accountingDate     记帐日期
     * @param ledgerAccount      帐目类型
     * @param incurred           发生金额
     */
    public LedgerEntry charge(PartyRole company, Dept dept, PartyRole partner, String inventoryMode,
                              String accountingMethod, String source, String sourceBusinessType,
                              Date accountingDate, LedgerAccount ledgerAccount, double incurred) {
        Ledger ledger = ledgerRepository.findLedger(company, dept, partner, inventoryMode, accountingMethod, ledgerAccount);
        if (ledger == null) {
            List<Ledger> ledgers = ledgerRepository.findLedger(company, partner, inventoryMode, accountingMethod, ledgerAccount);
            if (!ledgers.isEmpty()) {
                ledger = ledgers.stream().findFirst().get();
            }
        }
        if (ledger == null) {
            ledger = createLedger(company, dept, partner, inventoryMode, accountingMethod, ledgerAccount, incurred);
        } else {
            //账目类型为账期时 需要先清除原来的账期
            if (Objects.equals("period",ledgerAccount.getId())) ledger.setAmount(ledger.getAmount() * -1);

            ledger.setAmount(incurred);
            ledger = ledgerRepository.saveAndFlush(ledger);
        }
        return createLedgerEntry(ledger, source, sourceBusinessType, accountingDate, incurred);
    }

    /**
     * 事务性记帐
     *
     * @param parameters            输入条件（根据业务要求）
     * @param source                来源单号
     * @param sourceBusinessType    来源单据类型
     * @param accountingDate        记帐日期
     * @param sourceLedgerAccountId 帐目类型（减项）
     * @param targetLedgerAccountId 帐目类型（增项）
     * @param incurred              发生金额
     */
    public void transaction(Map<String, Object> parameters, String source, String sourceBusinessType, Date accountingDate, String sourceLedgerAccountId, String targetLedgerAccountId, double incurred) {
        if (incurred <= 0) throw new RuntimeException("帐目金额不允许小于零！");
        LedgerAccount sourceLedgerAccount = getLedgerAccount(sourceLedgerAccountId);
        parameters.put("ledgerAccount", sourceLedgerAccount);
        List<Ledger> ledgers = findLedgers(parameters);
        new BatchMethod<Batch>() {
            @Override
            public Iterable<Batch> wrap() {
                return ledgers.stream()
                        .filter(ledgers -> ledgers.getAmount() > 0)
                        .map(ledger -> {
                            Batch batch = new Batch();
                            batch.setEntity(ledger);
                            batch.setQty(ledger.getAmount());
                            return batch;
                        }).collect(Collectors.toList());
            }

            @Override
            public void succeed(Batch batch, double increment) {
                Ledger ledger = (Ledger) batch.getEntity();
                transaction(ledger.getCompany(), ledger.getDept(), ledger.getPartner(), ledger.getInventoryMode(),
                        ledger.getAccountingMethod(), source, sourceBusinessType, accountingDate,
                        ledger.getLedgerAccount(), -1 * increment, targetLedgerAccountId, increment);
            }

            @Override
            public void error(double surplus) {
                if (Arith.compare(surplus, 0, 2) != 0) {
                    throw new RuntimeException("总帐帐目：" + sourceLedgerAccount.getName() + " 余额不足 " +  new BigDecimal(surplus));
                }
            }
        }.execute(incurred);
    }

    /**
     * 事务性记帐
     *
     * @param company            公司
     * @param dept               部门
     * @param partner            合作伙伴
     * @param inventoryMode      库存方式
     * @param accountingMethod   采购核算方式
     * @param source             来源单号
     * @param sourceBusinessType 来源单据类型
     * @param accountingDate     记帐日期
     * @param items              帐目类型（id）, 发生金额, 帐目类型（id）, 发生金额,...
     */
    public void transaction(PartyRole company, Dept dept, PartyRole partner, String inventoryMode,
                            String accountingMethod, String source, String sourceBusinessType,
                            Date accountingDate, Object... items) {
        List<Account> accounts = parse(items);
        if (accounts.stream().collect(Collectors.groupingBy(Account::getLedgerGroup, Collectors.counting())).size() != 1) {
            throw new RuntimeException("同一事务中不允许有不同的帐目分组！");
        }
        for (Account account : accounts) {
            if (account.getIncurred() == 0) continue;  // 防止写入发生额为零的明细
            charge(company, dept, partner, inventoryMode, accountingMethod, source, sourceBusinessType, accountingDate, account.getLedgerAccount(), account.getIncurred());
        }
    }

    private Ledger createLedger(PartyRole company, Dept dept, PartyRole partner, String inventoryMode, String accountingMethod, LedgerAccount ledgerAccount, double incurred) {
        Ledger ledger;
        ledger = new Ledger();
        ledger.setCompany(company);
        ledger.setDept(dept);
        ledger.setPartner(partner);
        ledger.setInventoryMode(inventoryMode);
        ledger.setAccountingMethod(accountingMethod);
        ledger.setLedgerAccount(ledgerAccount);
        ledger.setAmount(incurred);
        return ledgerRepository.saveAndFlush(ledger);
    }

    private LedgerEntry createLedgerEntry(Ledger ledger, String source, String sourceBusinessType, Date accountingDate, double incurred) {
        LedgerEntry ledgerEntry = new LedgerEntry();
        ledgerEntry.setLedger(ledger);
        ledgerEntry.setSource(source);
        ledgerEntry.setSourceBusinessType(sourceBusinessType);
        ledgerEntry.setAccountingDate(accountingDate);
        ledgerEntry.setIncurred(incurred);
        ledgerEntry.setAmount(ledger.getAmount());
        setBusinessTypeName(ledgerEntry);
        return ledgerEntryRepository.saveAndFlush(ledgerEntry);
    }

    private List<Account> parse(Object... items) {
        Assert.isTrue(items.length % 2 == 0, "参数输入错误！");
        int i = 0;
        double total = 0;
        LedgerAccount ledgerAccount = null;
        double incurred;
        List<Account> m = new ArrayList<>();
        for (Object o : items) {
            if (i % 2 == 0) {
                ledgerAccount = getLedgerAccount(o);
            } else {
                incurred = (double) o;
                total += incurred;
                m.add(new Account(ledgerAccount, incurred));
            }
            i++;
        }
        if (Arith.round(total,2) != 0) throw new RuntimeException("帐目金额输入错误，合计不为零！");
        return m;
    }

    private LedgerAccount getLedgerAccount(Object o) {
        if (o instanceof LedgerAccount) return (LedgerAccount) o;
        if (o instanceof String) {
            LedgerAccount ledgerAccount = ledgerAccountRepository.findOne((String) o);
            Assert.notNull(ledgerAccount, String.format("帐目类型：%s 输入错误！", o));
            return ledgerAccount;
        }
        throw new RuntimeException(String.format("帐目类型：%s 输入错误！", String.valueOf(o)));
    }

    @Data
    private class Account {
        private LedgerAccount ledgerAccount;
        private double incurred;

        public Account(LedgerAccount ledgerAccount, double incurred) {
            this.ledgerAccount = ledgerAccount;
            this.incurred = incurred;
        }

        public LedgerGroup getLedgerGroup() {
            return ledgerAccount == null ? null : ledgerAccount.getLedgerGroup();
        }

    }

    @Autowired
    private BusinessTypeService businessTypeService;

    private void setBusinessTypeName(LedgerEntry le) {
        if (le == null) return;
        Class clazz = JpaUtil.getDomainClass(le.getSourceBusinessType());
        if (clazz == null) return;
        CustomRepository repository = JpaUtil.getRepository(clazz.getName());
        Object source = repository.findOne(JpaUtil.getId(le.getSourceBusinessType(), le.getSource()));
        BusinessType businessType = businessTypeService.getBusinessTypeByCache(le.getSourceBusinessType());
        if (businessType != null) {
            le.setSourceBusinessTypeName(businessType.getName());
        }
        String origin = ReflectUtil.getFieldValue(source, "origin");
        String originBusinessType = ReflectUtil.getFieldValue(source, "originBusinessType");
        if (!StringUtils.isEmpty(origin) && !StringUtils.isEmpty(originBusinessType)) {
            le.setOrigin(origin);
            le.setOriginBusinessType(originBusinessType);
            businessType = businessTypeService.getBusinessTypeByCache(originBusinessType);
            if (businessType != null) {
                le.setOriginBusinessTypeName(businessType.getName());
            }
        }
    }

    public List<LedgerAccount> findAll(SimpleRequest request) throws Exception {
        List<Map> condition = new ArrayList<>();
        List<String> arr = new ArrayList<>();
        arr.add("unsettled");
        arr.add("freeze");
        arr.add("provisional");
        condition.add(MapUtil.mapper("field", "usage.code", "value", arr, "operator", "IN"));
        Sort sort = new Sort(Sort.Direction.DESC, "sequence");
        List<LedgerAccount> ledgerAccounts = ledgerAccountRepository.findAll(JpaDsl.toCriteria("usage.code", "IN", arr, "show", "EQ", 1), sort);
        return ledgerAccounts;
    }

    public List<Map<String, Object>> findAllLedger(SimpleRequest request) {
        JSONObject json = request.getJO();
        List<Map<String, Object>> result = new ArrayList<>();
        try {
            List list = ledgerDao.findLedgerMains(json);
            Map group = (Map) list.stream().collect(groupingBy(item -> {
                Map map = (Map) item;
                Map returnMap = new HashMap();
                returnMap.put("DEPT_ID", map.get("DEPT_ID"));
                returnMap.put("COMPANY_NAME", map.get("COMPANY_NAME"));
                returnMap.put("DEPT_NAME", map.get("DEPT_NAME"));
                returnMap.put("ACCOUNTING_METHOD", map.get("ACCOUNTING_METHOD"));
                returnMap.put("PARTNER_NAME", map.get("PARTNER_NAME"));
                returnMap.put("INVENTORY_MODE", map.get("INVENTORY_MODE"));
                returnMap.put("COMPANY_ID", map.get("COMPANY_ID"));
                returnMap.put("PARTNER_ID", map.get("PARTNER_ID"));
                returnMap.put("COOPERATION", map.get("COOPERATION"));
                return returnMap;
            }));
            for (Object key : group.keySet()) {
                Map mapKey = (Map) key;
                List<Map> listValues = (List) group.get(key);
                Map value = listValues.stream().collect(HashMap::new, (m, v) ->
                        m.put(v.get("LEDGER_ACCOUNT_ID"), v.get("AMOUNT")), HashMap::putAll);
                mapKey.putAll(value);
                result.add(mapKey);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return result;
    }


    public Page<LedgerEntry> findAllLedgerEntry(SimpleRequest request) {
        JSONObject json = request.getJO();
        String COMPANY_ID = json.getString("COMPANY_ID");
        String DEPT_ID = json.getString("DEPT_ID");
        String PARTNER_ID = json.getString("PARTNER_ID");
        String ACCOUNTING_METHOD = json.getString("ACCOUNTING_METHOD");
        String INVENTORY_MODE = json.getString("INVENTORY_MODE");
        String LEDGER_ACCOUNT_ID = json.getString("LEDGER_ACCOUNT_ID");
        String ACCOUNTING_DATE_BEGIN = json.getString("ACCOUNTING_DATE_BEGIN");
        String ACCOUNTING_DATE_END = json.getString("ACCOUNTING_DATE_END");

//        return ledgerEntryRepository.findLedgerEntryList(COMPANY_ID,PARTNER_ID,DEPT_ID,INVENTORY_MODE,ACCOUNTING_METHOD,LEDGER_ACCOUNT_ID,
//                ACCOUNTING_DATE_BEGIN,ACCOUNTING_DATE_END,PageRequest.of(request.get("page"), request.get("size")));
        return null;
    }


    @Autowired
    private EntityManager em;

    /**合并（调帐使用）**/
    public Map test() {
        String sql = "SELECT COMPANY_ID, PARTNER_ID, LEDGER_ACCOUNT_ID, INVENTORY_MODE, ACCOUNTING_METHOD, COUNT(*) JL\n" +
                "  FROM ACC_LEDGER\n" +
                " GROUP BY COMPANY_ID, PARTNER_ID, LEDGER_ACCOUNT_ID, INVENTORY_MODE, ACCOUNTING_METHOD\n" +
                "HAVING COUNT(*) > 1";
        em.createNativeQuery(sql).unwrap(NativeQueryImpl.class)
                .setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP)
                .getResultStream()
                .forEach(m -> {
                    Map<String, Object> parameters = new HashMap<>();
                    parameters.put("company.id", ((Map) m).get("COMPANY_ID"));
                    parameters.put("partner.id", ((Map) m).get("PARTNER_ID"));
                    parameters.put("ledgerAccount.id", ((Map) m).get("LEDGER_ACCOUNT_ID"));
                    parameters.put("inventoryMode", ((Map) m).get("INVENTORY_MODE"));
                    parameters.put("accountingMethod", ((Map) m).get("ACCOUNTING_METHOD"));

                    List<Ledger> ledgers = ledgerRepository.findAll(JpaDsl.toCriteria(parameters), new Sort(Sort.Direction.DESC, "version"));
                    Ledger source = null, target;
                    for (int i = 0; i < ledgers.size(); i++) {
                        if (i == 0) {
                            source = ledgers.get(i);
                        } else {
                            target = ledgers.get(i);
                            source.setAmount(target.getAmount());
                            source = ledgerRepository.saveAndFlush(source);
                            List<LedgerEntry> ledgerEntries = ledgerEntryRepository.findAll(JpaDsl.toCriteriaByEq("ledger", target));
                            for (LedgerEntry le : ledgerEntries) {
                                le.setLedger(source);
                                ledgerEntryRepository.save(le);
                            }
                            ledgerRepository.delete(target);
                        }
                    }
                    List<LedgerEntry> ledgerEntries = ledgerEntryRepository.findAll(JpaDsl.toCriteriaByEq("ledger", source), new Sort(Sort.Direction.ASC, "lastUpdatedStamp"));
                    int j = 0;
                    double amount = 0;
                    for (LedgerEntry le : ledgerEntries) {
                        if (j == 0) {
                            amount = le.getAmount();
                        } else {
                            amount = amount + le.getIncurred();
                            le.setAmount(amount);
                            ledgerEntryRepository.save(le);
                        }
                        j ++;
                    }
                });
        return new HashMap();
    }

    /**
     * 应收账龄分析
     * @param request
     * @return
     */
    public JSONObject agingReceivableAnalyze(SimpleRequest request) {
        JSONObject param = new JSONObject();
        PartyRole currentCompany = (PartyRole) request.getCurrentCompany();
        param.put("COMPANY_ID",currentCompany.getId());
        List<Map> conditions = (List<Map>) request.get("condition");
        conditions.forEach(condition -> {
            param.put((String) condition.get("field"), condition.get("value").toString());
        });
        int page = (int) request.getJO().get("page");
        int size = (int) request.getJO().get("size");
        param.put("page",page+1);
        param.put("size",size);
        JSONObject object = new JSONObject();
        if (param.containsKey("ACCOUNTING_DATE") && param.get("ACCOUNTING_DATE") != null) {
            param.put("ACCOUNTING_DATE", param.getDate("ACCOUNTING_DATE"));
        }
        if (!param.containsKey("ACCOUNTING_DATE") ) {
            param.put("ACCOUNTING_DATE", new Date());
        }
        if (param.getString("TYPE").equals("all")) param.remove("TYPE");
        object.put("content", ledgerDao.findAgingReceivableByPage(param));
        List<HashMap> agingReceivable = ledgerDao.findAgingReceivable(param);
        Object count = agingReceivable.size();
        object.put("total", count);
        object.put("pages", count);
        //计算合计
        param.put("page",1);
        param.put("size",count);
        object.put("footer", Arrays.asList(MapUtil.mapper("ENDING_BALANCE", agingReceivable.stream().map(x -> Double.valueOf(x.get("ENDING_BALANCE").toString())).reduce(Arith::add).orElse(0.0),
                "SEVEN_DAY_BALANCE", agingReceivable.stream().map(x -> Double.valueOf(x.get("SEVEN_DAY_BALANCE").toString())).reduce(Arith::add).orElse(0.0),
                "EIGHT_DAY_BALANCE", agingReceivable.stream().map(x -> Double.valueOf(x.get("EIGHT_DAY_BALANCE").toString())).reduce(Arith::add).orElse(0.0),
                "THIRTY_DAY_BALANCE", agingReceivable.stream().map(x -> Double.valueOf(x.get("THIRTY_DAY_BALANCE").toString())).reduce(Arith::add).orElse(0.0),
                "SIXTY_DAY_BALANCE", agingReceivable.stream().map(x -> Double.valueOf(x.get("SIXTY_DAY_BALANCE").toString())).reduce(Arith::add).orElse(0.0),
                "NINETY_DAY_BALANCE", agingReceivable.stream().map(x -> Double.valueOf(x.get("NINETY_DAY_BALANCE").toString())).reduce(Arith::add).orElse(0.0),
                "HUNDRED_EIGHTY_DAY_BALANCE", agingReceivable.stream().map(x ->Double.valueOf(x.get("HUNDRED_EIGHTY_DAY_BALANCE").toString())).reduce(Arith::add).orElse(0.0),
                "WITHIN_YEAR_BALANCE", agingReceivable.stream().map(x -> Double.valueOf(x.get("WITHIN_YEAR_BALANCE").toString())).reduce(Arith::add).orElse(0.0),
                "OUTSIDE_YEAR_BALANCE", agingReceivable.stream().map(x -> Double.valueOf(x.get("OUTSIDE_YEAR_BALANCE").toString())).reduce(Arith::add).orElse(0.0)
        )));
        return object;
    }

}
